Ctrl+K

بارگذاری دارایی‌ها در وب

هنگام ساخت بازی برای وب با موتور بازی توت فرنگی، بارگذاری دارایی‌ها متفاوت از پلتفرم‌های دسکتاپ یا موبایل کار می‌کند. از آنجا که مرورگر دسترسی مستقیمی به سیستم فایل محلی ندارد، تمام دارایی‌های بازی — مانند بافت‌ها، صداها، فونت‌ها و فایل‌های داده — باید پیش از استفاده به‌صورت صریح دانلود شوند. توت فرنگی یک خط لوله دارایی ساده‌شده مخصوص این محیط ارائه می‌دهد که دریافت، کش و خواندن دارایی‌ها در زمان اجرا را آسان می‌کند.

این مستند نحوه دانلود دارایی‌ها پیش از موعد، نحوه دسترسی به آن‌ها پس از آماده‌شدن، و بهترین روش‌ها برای ساختاردهی جریان بارگذاری را پوشش می‌دهد.


چرا بارگذاری دارایی‌ها در وب متفاوت است

در پلتفرم‌های دسکتاپ، بازی شما می‌تواند فایل‌ها را مستقیماً از دیسک با استفاده از I/O استاندارد فایل بخواند. سیستم‌عامل یک سیستم فایل ارائه می‌دهد که دارایی‌ها در کنار فایل اجرایی شما قرار دارند و می‌توانید آن‌ها را در هر زمانی باز کنید. اما مرورگر بازی شما را در محیطی ایزوله اجرا می‌کند که دسترسی به سیستم فایل میزبان ندارد. هر چیزی که بازی شما نیاز دارد باید از طریق HTTP از سرور منتقل شود.

این موضوع چند پیامد مهم دارد:

  • دارایی‌ها باید دانلود شوند، نه از دیسک خوانده شوند. هر بافت، فایل صوتی، فونت و فایل داده باید پیش از آنکه کد بازی بتواند از آن استفاده کند، از URL راه دور دریافت شود.
  • دانلودها ناهمزمان هستند. درخواست‌های شبکه زمان می‌برند و ممکن است با شکست مواجه شوند، بنابراین منطق بارگذاری شما باید ناهمزمانی را به‌خوبی مدیریت کند.
  • صفحات بارگذاری ضروری هستند. برخلاف دسکتاپ که خواندن فایل تقریباً فوری است، دانلودهای وب بسته به اندازه فایل و شرایط شبکه ممکن است ثانیه‌ها طول بکشد. باید بازیکن را در جریان وضعیت بارگذاری قرار دهید.
  • رفتار کش به مرورگر بستگی دارد. پس از دانلود، دارایی‌ها ممکن است توسط مرورگر کش شوند، اما نمی‌توانید به این موضوع در تمام نشست‌ها تکیه کنید. کد بارگذاری شما باید همیشه فرض کند که دارایی‌ها نیاز به دریافت دارند.

توت فرنگی بخش عمده‌ای از این پیچیدگی را با ارائه کلاس GameLauncher برای وب انتزاع می‌کند که دانلود دارایی‌ها و در دسترس قرار دادن آن‌ها از طریق رابط IStorage را مدیریت می‌کند.


دانلود AOT دارایی‌ها

AOT مخفف Ahead-Of-Time به معنای «پیش از موعد» است، یعنی دارایی‌ها پیش از شروع اجرای بازی دانلود می‌شوند. این رایج‌ترین و توصیه‌شده‌ترین روش برای بارگذاری دارایی‌ها در بازی وب توت فرنگی است. دانلود AOT تضمین می‌کند که هر منبعی که بازی نیاز دارد پیش از رندر شدن اولین فریم در حافظه موجود باشد و خطر از دست رفتن دارایی‌ها در زمان اجرا را از بین می‌برد.

دانلود AOT به‌ویژه برای موارد زیر مناسب است:

  • فونت‌ها — رندر متن نیازمند در دسترس بودن فوری داده‌های فونت است؛ نمی‌توانید جایگزینی برای حروف گم‌شده نمایش دهید.
  • بافت‌های اصلی و اطلس‌های اسپرایت — پایه بصری بازی شما باید پیش از رسم هر صحنه‌ای آماده باشد.
  • فایل‌های پیکربندی و داده — منطق بازی اغلب به فایل‌های داده‌ای (طراحی مراحل، تعریف آیتم‌ها و غیره) وابسته است که باید پیش از شروع گیم‌پلی تجزیه شوند.
  • افکت‌های صوتی ضروری — صداهایی که در مراحل اولیه گیم‌پلی پخش می‌شوند باید پیش‌تر بارگذاری شوند تا از سکوت یا تأخیر جلوگیری شود.

نحوه کار دانلود AOT

کلاس Strawberry.Web.GameLauncher متد AOTDownload را ارائه می‌دهد که یک دارایی منفرد را از سرور دریافت و در حافظه ذخیره می‌کند. هر فراخوانی AOTDownload یک Task برمی‌گرداند که پس از تکمیل دانلود تکمیل می‌شود. از آنجا که هر دانلود یک تسک مستقل است، می‌توانید چندین دارایی را به‌صورت موازی با Task.WhenAll دانلود کنید که زمان بارگذاری کل را در مقایسه با دانلود متوالی به‌طور قابل‌توجهی کاهش می‌دهد.

در اینجا یک مثال کامل از راه‌اندازی دانلود AOT برای بازی SpaceShooter آورده شده است:

using System.Runtime.Versioning;
using System.Threading.Tasks;
using SpaceShooter;
using Strawberry;
using Strawberry.Web;

[assembly: SupportedOSPlatform("browser")]

public static class Program
{
    public static async Task Main(string[] args)
    {
        SpaceShooterGameContext gameContext = new SpaceShooterGameContext(240, 320);
        Game game = new Game();
        var l = new GameLauncher();
        await Task.WhenAll(
            l.AOTDownload("atlas.sbTex"),
            l.AOTDownload("atlas.sprList"),
            l.AOTDownload("bullet-laser.wav"),
            l.AOTDownload("explosion-small.wav"),
            l.AOTDownload("music01.ogg"),
            l.AOTDownload("hesab.font"),
            l.AOTDownload("elm.font"),
            l.AOTDownload("powerup.wav")
        );
        game.Run(gameContext, l);
    }
}

تحلیل مثال

  1. [assembly: SupportedOSPlatform("browser")] — این ویژگی به کامپایلر اعلام می‌کند که اسمبلی پلتفرم مرورگر را هدف قرار داده است. این ویژگی برای دسترسی به API‌های مخصوص وب بدون هشدار کامپایلر ضروری است.

  2. new GameLauncher() — یک نمونه از راه‌انداز بازی مخصوص وب ایجاد می‌کند. این راه‌انداز نقطه ورود مرورگر، دانلود دارایی‌ها و مقداردهی بازی را مدیریت می‌کند.

  3. l.AOTDownload("filename") — هر فراخوانی یک درخواست HTTP برای دانلود دارایی مشخص‌شده ارسال می‌کند. نام دارایی باید دقیقاً با نام فایل موجود در سرور مطابقت داشته باشد. این متد یک Task برمی‌گرداند که پس از تکمیل دانلود حل می‌شود.

  4. Task.WhenAll(...) — تمام تسک‌های دانلود را در یک تسک ترکیب می‌کند که تنها زمانی تکمیل می‌شود که هر دانلود منفرد به پایان رسیده باشد. این امکان دانلود موازی را فراهم می‌کند — مرورگر می‌تواند چندین فایل را همزمان دریافت کند و فرآیند بارگذاری کلی را بسیار سریع‌تر کند.

  5. await Task.WhenAll(...) — انتظار تکمیل تمام دانلودها را می‌کشد. بازی از این خط عبور نخواهد کرد تا زمانی که تمام دارایی‌ها آماده باشند. این همان چیزی است که بارگذاری را «پیش از موعد» می‌کند.

  6. game.Run(gameContext, l) — پس از دانلود تمام دارایی‌ها، بازی شروع می‌شود. راه‌انداز تضمین می‌کند که دارایی‌های دانلودشده از طریق سیستم ذخیره‌سازی قابل دسترسی هستند.

مدیریت پیشرفت بارگذاری

از آنجا که Task.WhenAll تا تکمیل تمام دانلودها مسدود می‌شود، می‌توانید از آن به‌عنوان نقطه‌ای طبیعی برای نمایش یا به‌روزرسانی صفحه بارگذاری استفاده کنید. اگر می‌خواهید گزارش پیشرفت دقیق‌تری داشته باشید — مثلاً برای نمایش نوار پیشرفت — می‌توانید به‌جای آن تسک‌های دانلود منفرد را پیگیری کنید:

var downloadTasks = new List<Task>
{
    l.AOTDownload("atlas.sbTex"),
    l.AOTDownload("atlas.sprList"),
    l.AOTDownload("bullet-laser.wav"),
    l.AOTDownload("explosion-small.wav"),
    l.AOTDownload("music01.ogg"),
    l.AOTDownload("hesab.font"),
    l.AOTDownload("elm.font"),
    l.AOTDownload("powerup.wav")
};

int total = downloadTasks.Count;
int completed = 0;

while (completed < total)
{
    var finished = await Task.WhenAny(downloadTasks);
    downloadTasks.Remove(finished);
    completed++;
    float progress = (float)completed / total;
    // صفحه بارگذاری خود را با 'progress' (0.0 تا 1.0) به‌روزرسانی کنید
}

این روش به شما اجازه می‌دهد پیشرفت تدریجی را به بازیکن گزارش دهید، که به‌ویژه برای بازی‌هایی با بسته‌های دارایی بزرگ که دانلود آن‌ها ممکن است چندین ثانیه طول بکشد مفید است.

مدیریت خطا هنگام دانلود

درخواست‌های شبکه ممکن است به دلایل مختلفی با شکست مواجه شوند: خطاهای سرور، اتصال قطع‌شده یا پاسخ‌های خراب. خوب است که منطق دانلود خود را در بلوک‌های try-catch قرار دهید تا بازی بتواند به‌خوبی به شکست‌ها واکنش نشان دهد:

try
{
    await Task.WhenAll(
        l.AOTDownload("atlas.sbTex"),
        l.AOTDownload("atlas.sprList"),
        l.AOTDownload("music01.ogg")
    );
}
catch (Exception ex)
{
    // ثبت خطا و اطلاع‌رسانی به بازیکن
    Console.WriteLine($"Asset download failed: {ex.Message}");
    // اختیاری: تلاش مجدد یا استفاده از مجموعه دارایی حداقلی
}

هنگامی که دانلودی با شکست مواجه می‌شود، تسک AOTDownload خطا می‌دهد و استثنا (Exception) از طریق Task.WhenAll منتقل می‌شود. گرفتن آن به شما امکان می‌دهد پیام خطا نمایش دهید، دانلود را مجدداً تلاش کنید یا سعی کنید با مجموعه‌ای کاهش‌یافته از دارایی‌ها ادامه دهید.


استفاده از دارایی‌های دانلودشده

پس از دانلود دارایی‌ها (چه از طریق دانلود AOT یا مکانیزم دیگر)، آن‌ها از طریق سیستم ذخیره‌سازی ارائه‌شده توسط Strawberry.Core.IGameContext.Storage در دسترس هستند. ویژگی Storage به شما دسترسی به نمونه‌ای از Strawberry.Misc.IStorage می‌دهد که بر داده‌های دارایی کش‌شده مرورگر انتزاع شده است.

دو متد اصلی برای خواندن دارایی‌های دانلودشده وجود دارد:

باز کردن جریان با Storage.Open

متد Open یک Stream استاندارد سی‌شارپ برمی‌گرداند که می‌توانید از آن برای خواندن تدریجی داده‌های دارایی استفاده کنید. این روش در موارد زیر مفید است:

  • می‌خواهید فقط بخشی از یک فایل بزرگ را بدون بارگذاری کل آن در حافظه بخوانید.
  • با کتابخانه یا API‌ای کار می‌کنید که Stream را به‌عنوان ورودی می‌پذیرد (مانند رمزگشاهای صوتی).
  • نیاز به تجزیه فرمت فایل سفارشی دارید که ابتدا هدرها را می‌خوانید و سپس بخش‌های خاصی از داده را به‌صورت انتخابی می‌خوانید.
public class MyGameContext : StdGameContext
{
    ...
    public override void OnInitialize(IGameLauncher launcher) {
        ...
        // باز کردن جریان برای فایل موسیقی — OggReader از جریان می‌خواند
        var music01 = SoundManager.CreateStream(new OggReader(Storage.Open("music01.ogg")));
        ...
    }
    ...
}

در این مثال، Storage.Open("music01.ogg") جریانی به فایل صوتی OGG ارائه می‌دهد که سپس به OggReader برای رمزگشایی ارسال می‌شود. رویکرد جریانی به رمزگشای صوتی اجازه می‌دهد داده‌ها را به‌صورت تکه‌ای بخواند به‌جای اینکه کل فایل ابتدا در آرایه بایتی بارگذاری شود.

خواندن کل فایل با Storage.ReadAllBytes

متد ReadAllBytes تمام محتوای یک دارایی را در آرایه byte[] می‌خواند. این ساده‌ترین روش بارگذاری دارایی است و زمانی ایده‌آل است که:

  • فایل نسبتاً کوچک است و به‌راحتی در حافظه جا می‌شود.
  • API مصرف‌کننده آرایه بایتی را به‌جای جریان انتظار دارد.
  • نیاز به دسترسی تصادفی به کل محتوای فایل دارید.
public class MyGameContext : StdGameContext
{
    ...
    public override void OnInitialize(IGameLauncher launcher) {
        ...
        // خواندن کل فایل فونت در حافظه به‌صورت آرایه بایتی
        Font = new Font(GraphicsContext, Storage.ReadAllBytes("elm.font"));
        ...
    }
    ...
}

در اینجا، Storage.ReadAllBytes("elm.font") فایل فونت کامل را در آرایه بایتی بارگذاری می‌کند که سپس به سازنده Font ارسال می‌شود. این روش ساده است و برای فایل‌هایی که به‌صورت کامل استفاده می‌شوند خوب کار می‌کند.

انتخاب بین Open و ReadAllBytes

ملاحظه Storage.Open Storage.ReadAllBytes
مصرف حافظه کمتر — داده‌ها بر اساس نیاز خوانده می‌شوند بیشتر — کل فایل در حافظه بارگذاری می‌شود
مناسب فایل‌های بزرگ بله — از بارگذاری همه‌چیز به‌یکباره جلوگیری می‌کند خیر — ممکن است فشار حافظه ایجاد کند
مناسب فایل‌های کوچک کار می‌کند اما پیچیدگی غیرضروری اضافه می‌کند ساده‌تر و به همان اندازه کارآمد
API‌های مبتنی بر جریان ضروری — Stream را مستقیماً ارسال می‌کند مناسب نیست — باید بایت‌ها در MemoryStream پیچیده شوند
دسترسی تصادفی نیاز به جستجو در جریان دارد داخلی — آرایه بایتی از ایندکس‌گذاری پشتیبانی می‌کند
سادگی کد بیشتر برای مدیریت چرخه حیات جریان یک خطی — فوراً داده را برمی‌گرداند

به‌عنوان قاعده کلی، از ReadAllBytes برای دارایی‌های کوچک تا متوسط (فونت‌ها، بافت‌های کوچک، فایل‌های پیکربندی) و از Open برای دارایی‌های بزرگ (فایل‌های موسیقی، داده‌های ویدیویی، اطلس‌های اسپرایت بزرگ) یا هنگام کار با API‌های مبتنی بر جریان استفاده کنید.

حل نام فایل

پارامتر ورودی هر دو متد Open و ReadAllBytes همیشه نام دقیق فایل است که هنگام دانلود استفاده شده است. اگر دارایی‌ای را به‌صورت l.AOTDownload("atlas.sbTex") دانلود کرده‌اید، با Storage.Open("atlas.sbTex") یا Storage.ReadAllBytes("atlas.sbTex") به آن دسترسی پیدا می‌کنید. نام به بزرگی و کوچکی حروف حساس است و شامل هیچ پیشوند مسیری نمی‌شود — صرفاً نام فایلی است که در مرحله دانلود ثبت شده.

اگر سعی کنید به دارایی‌ای دسترسی پیدا کنید که دانلود نشده است، سیستم ذخیره‌سازی استثنایی (Exception) نشان‌دهنده پیدا نشدن فایل پرتاب (Throw) می‌کند. همیشه مطمئن شوید که هر دارایی‌ای که بازی شما به آن ارجاع می‌دهد در لیست دانلود AOT گنجانده شده است.


بهترین روش‌ها

سازماندهی لیست دارایی‌ها

تمام فراخوانی‌های دانلود AOT را در یک مکان نگه دارید — معمولاً متد Main یا یک متد استاتیک اختصاصی. این کار بررسی دارایی‌های مورد نیاز بازی را آسان می‌کند و از پراکندگی منطق دانلود که نگهداری آن دشوار است جلوگیری می‌کند:

private static async Task DownloadAllAssets(GameLauncher launcher)
{
    await Task.WhenAll(
        // بافت‌ها
        launcher.AOTDownload("atlas.sbTex"),
        launcher.AOTDownload("atlas.sprList"),

        // صدا
        launcher.AOTDownload("bullet-laser.wav"),
        launcher.AOTDownload("explosion-small.wav"),
        launcher.AOTDownload("music01.ogg"),
        launcher.AOTDownload("powerup.wav"),

        // فونت‌ها
        launcher.AOTDownload("hesab.font"),
        launcher.AOTDownload("elm.font")
    );
}

گروه‌بندی دارایی‌ها بر اساس دسته‌بندی (بافت‌ها، صدا، فونت‌ها، داده) با نظرات، لیست را خودمستندساز می‌کند و به‌روزرسانی آن با رشد بازی آسان‌تر می‌شود.

کاهش اندازه کل دارایی‌ها

هر بایت دانلودشده به زمان بارگذاری بازی اضافه می‌شود. در وب، بازیکنان صبر محدودی برای صفحات بارگذاری دارند، بنابراین مهم است که بسته دارایی‌های خود را تا حد امکان سبک نگه دارید:

  • فشرده‌سازی بافت‌ها — از فرمت‌های بافت پشتیبانی‌شده توسط Strawberry (مانند .sbTex) که شامل فشرده‌سازی هستند استفاده کنید.
  • استفاده از اطلس‌های اسپرایت — به‌جای دانلود تصاویر کوچک متعدد، آن‌ها را در یک بافت اطلس واحد ترکیب کنید. این تعداد درخواست‌های HTTP را کاهش می‌دهد و اغلب به اندازه کل فایل کوچک‌تری منجر می‌شود.
  • بهینه‌سازی صدا — از کدک‌های صوتی کارآمد استفاده کنید (OGG برای موسیقی، WAV برای افکت‌های صوتی کوتاه) و سکوت را از فایل‌های صوتی حذف کنید.
  • اجتناب از دارایی‌های استفاده‌نشده — به‌طور منظم لیست دانلود خود را بررسی کنید و دارایی‌هایی که دیگر در بازی استفاده نمی‌شوند را حذف کنید.

تفکیک دارایی‌های ضروری و اختیاری

همه دارایی‌ها لازم نیست پیش از شروع بازی در دسترس باشند. دارایی‌های خود را به دو دسته تقسیم کنید:

  • دارایی‌های ضروری — برای عملکرد بازی لازم هستند (بافت‌های اصلی، فونت‌ها، صداهای ضروری). این‌ها را با AOT دانلود کنید.
  • دارایی‌های اختیاری — بعداً در بازی استفاده می‌شوند (موسیقی پس‌زمینه مراحل بعدی، دارایی‌های سینمایی، محتوای پاداش). این‌ها می‌توانند به‌صورت تقاضایی پس از شروع بازی بارگذاری شوند، اگر معماری شما از آن پشتیبانی کند.

با به تعویق انداختن دارایی‌های غیرضروری، زمان بارگذاری اولیه را کاهش می‌دهید و بازیکن سریع‌تر وارد بازی می‌شود.

تست روی اتصال‌های کند

همیشه جریان بارگذاری خود را روی اتصال‌های شبکه محدودشده تست کنید تا شرایط واقعی را شبیه‌سازی کنید. آنچه روی اتصال سریع در کمتر از یک ثانیه بارگذاری می‌شود، ممکن است برای بازیکنان روی شبکه‌های موبایل یا در مناطق با اینترنت کند بسیار طولانی‌تر باشد. یک نوار پیشرفت یا نشانگر بارگذاری متحرک تأثیر زیادی در حفظ تعامل بازیکنان در طول دانلودهای طولانی دارد.